試想,鄉親在 LINE 先問花壇場次的集合點,隨後留下「需要手語志工支援」的詢問,看完內容、按下確認。過一會兒再問「剛才那單有成功嗎?」時,接電話的已經換成另一個雲端行程 (Process) ,它還認得原本那件事嗎?今天把 LINE、Google ADK 與 Firestore 接到 Cloud Run,準備用同一張請求核對答案。
Day 12 connects LINE, an ADK-based activity lookup, and the persistent request service in a Cloud Run application. Gemini interprets search conditions; the application binds confirmation to an operation and reads its receipt from Firestore. The deployment experiment verifies that the same request ID is recovered across Cloud Run revisions.
像服務窗口換班,接班的人不需要把上一位志工的腦袋搬過來,只要找得到原詢問、當時的確認與現在進度,就能繼續說明。Day 11 已經把這些紀錄移出單一行程;今天要讓手機使用者也用得到。
以下是今天真實雲端閉環的完整驗收歷程:
| 使用者做什麼 | LOCAL 做什麼 | 這一步實際看見什麼 |
|---|---|---|
| 問「花壇場次在哪裡集合?」 | Gemini 3.8 Flash 理解意圖,工具查採用資料 | 首次查詢暫不可用時明確提示;第二次查到活動地點與未提供集合點 |
| 輸入「需要協助:需要手語志工支援」 | 保存原文,發起二階段確認卡片 | 帶有原確認識別與 5 分鐘時限的「確認送出這份詢問」按鈕 |
| 點下確認 | 核對原內容與目前權限,正式寫入 Firestore | 實際產生專屬單號 req-20260926-85daad15417821ee 與待處理狀態 |
| 部署同一映像的新修訂版 | 將 100% 流量切換交給全新的修訂版行程 | 最新修訂版 local-day12-agent-00004-zz6 就緒,行程記憶體全新啟動 |
| 送出「剛才那單有成功嗎?」 | 依持久化參照由 Firestore 查原單 | 成功讀回同一單號 req-20260926-85daad15417821ee,證明跨行程任務接續 |

圖 1:LINE 教學帳號的詢問、確認與原單查回。左圖第一次查詢沒有取得工具結果,程式說明並保留原單查詢,第二次查到活動地點;輸入需求後出示確認卡,點確認建立請求 req-20260926-85daad15417821ee。右圖為部署新修訂版之後,查回同一單號與狀態。
我想讓讀者記住的成果是: 換了處理的人,使用者也不必把整件事再說一次。 單號要從資料庫讀回,不能靠模型把上次那句話背出來。
我們團隊負責「卦山大縱走」的 LINE 數位集章與抽獎系統,所以我選這個熟悉的地方活動作為 LOCAL 的教材背景。正因為扛過真實地方活動的線上服務,我深刻體會到無伺服器環境的維運痛點:在允許縮容至零(Scale to Zero)的設定下,若一段時間沒有流量,閒置實例會被平台回收;下一個請求在全新的實例上冷啟動,如果程式把對話狀態留在記憶體變數裡,換了實例就會失去紀錄,叫鄉親從頭填一次。
我想處理的是一個很具體的問題:鄉親已經交代過的詢問,後端換了行程之後,能不能找回原單,而不是請他再說一次?
這次示範使用專屬的 LOCAL 教學帳號與 9 月 19 日花壇場次的歷史快照,聚焦於查詢、確認、保存詢問與跨行程查回。我們保存的是任務參照與回條單號,而不是把整段聊天記憶背出來;請求建立後的狀態為「待真人處理」,本示範尚未發出外部通知或派遣志工。
雲端演練採用最明確的「修訂版流量切換」來驗證新行程接續,本次不宣稱已觀察到自然縮容至零;Day 11 的模擬器結果是本機邏輯基礎,不能代替真正的雲端權限與網路傳輸。
收到使用者的自然語言提問後,我把原句交給 ADK 的 LlmAgent。Gemini 3.8 Flash 理解問題並選擇搜尋參數,再呼叫 search_local_events;這次沒有把正確參數先填給它。[3]
真實連線的手機測試中,對話記錄如下:
| 時間 | 發生什麼 | 系統選擇 |
|---|---|---|
| 18:02 | 工具未取得可核對結果 | 程式回覆「這次查詢暫時沒有取得可核對的工具結果。原單查詢仍可使用。」不瞎編集合點,保留查原單防線 |
| 18:06 | 工具回傳花壇快照 | 標示教學資料、明示活動時段與地點,並指出集合時間與集合點未提供 |
| 18:09–18:12 | 使用者留下手語需求並確認 | 出示確認卡,使用者點擊確認後正式寫入 Firestore,產生單號 req-20260926-85daad15417821ee |
| 18:15(換修訂版後) | 送出「剛才那單有成功嗎?」 | 新行程自 Firestore 讀回同一單號與狀態,未重開確認 |
在 18:02 的那次嘗試中,查詢未取得通過核對的工具結果,程式因此回覆暫不可用,並保留原單查詢入口。這和「工具成功查詢,但沒有符合條件的資料」是不同結果。系統沒有憑空捏造集合點,也沒有把失敗假裝成成功;這是受控的降級。
工具回傳後,手機上的時間、地點、來源與未知欄位由程式組合。這個版本讓 Gemini 負責理解搜尋條件;確認與單號則維持可以直接核對的格式。每次查詢至多兩次工具呼叫、三次模型呼叫。固定的「查詢原單」按鈕直接走查回服務,省下一次無必要的模型請求。
手機上的確認分成兩步。收到志工需求時,先保存待確認內容,此時尚未建立正式請求;按鈕的 postback 帶回原 confirmation_id,後端從已驗簽事件核對本人、原內容與期限,再交給建單服務。[4][5]
節錄自程式核心(tasks.py):
args = self.original.prepare(
actor, operation,
ttl_seconds=300,
approved=False,
idempotency_key=send_key,
)
approved=False 表示提出確認時尚未獲得使用者同意。即使點下按鈕,產生的確認紀錄中 execution_allowed 仍維持 false,由後端建單服務核對目前權限後才寫入 Firestore。查原單則直接取回既有請求,不會重新要求一次同意。我寧願把現在的進度說準,也不讓一張正確的單號,搭上一句尚未安排的承諾。
在 Cloud Run 上執行容器,平台依即時流量調度執行個體。容器內部監聽平台指定的環境變數 $PORT(預設為 8080),並綁定 0.0.0.0。本篇使用 FastAPI 實作,提供接收 LINE 訊息的 /webhook 與平台健康檢查的 /healthz 端點。[1]
LINE → Cloud Run 驗簽與路由 → ADK/Gemini 查詢,或原確認與建單服務 → Firestore → LINE 回覆。
關於回應時限,需要特別說明:LINE 官方文件指出,Webhook 伺服器若未於 2 秒 內回應 HTTP,將在統計中記為 request_timeout。[6] 目前本篇採少量測試白名單的同步示範,模型流程、資料操作與 Reply 都在 HTTP 200 前完成。模型流程可能接近或超過此時限,因此手機收到回覆,不能代替 Webhook 延遲的檢查。後續篇章支援較慢任務時,再將快速接收與持久工作處理切開。
--min-instances 0 讓服務在完全沒有流量時允許縮容至零。在今天的驗證中,我們先採用最明確、可重複驗證的工程手段:先在修訂版 A 建立請求,接著部署相同映像的新修訂版 B 切換 100% 流量,再從手機查回原單。
這樣就把兩個問題清晰拆開:第一,資料能不能被全新行程接續;第二,平台在什麼時機點回收行程。
在本地端開發時,我們習慣建立 .env 檔案存放密鑰。但當程式打包上雲端時,把 .env 包進 Docker 映像檔或是明碼寫在設定檔中,是常見的安全隱患。
本篇選擇自己撰寫 Dockerfile,是為了把相依安裝放在建置階段,並配置非 root 的專用使用者(appuser,UID 10001),讓容器不在高權限身分下運作。建置階段使用獨立的上下文,並透過 pip check 檢查已安裝套件宣告的相依條件後記錄其 digest(本例為 sha256:2ab1848373...),部署時直接鎖定這組不變的指紋。程式匯入與整合行為另由測試核對。
金鑰與身分管理則交給 Google Cloud 的身分與秘密管理機制:
local-day12-runtime,只授予讀寫 Firestore 的 roles/datastore.user [8] 與存取 5 組機密的 roles/secretmanager.secretAccessor [7],不給予專案的 Owner 或 Editor,將資料影響面控制在專用測試專案內。:1),不用 latest,避免非預期的金鑰輪替導致修訂版行為飄移。
圖 2:本次建立的 Secret Manager 項目清單,畫面未顯示秘密值。服務帳戶能讀哪些項目,需另外由 IAM 與 Cloud Run 秘密引用設定核對。
部署時使用下列命令範例:
gcloud run deploy "$SERVICE" --project "$PROJECT_ID" --region "$REGION" \
--image "$IMAGE_DIGEST" --service-account "$RUNTIME_SA" \
--allow-unauthenticated --ingress all \
--min-instances 0 --max-instances 2 \
--memory 512Mi --cpu 1 --concurrency 4 --timeout 60s \
--env-vars-file "$ENV_FILE" --set-secrets "$SECRETS_BINDINGS" \
--startup-probe='httpGet.path=/healthz,httpGet.port=8080,timeoutSeconds=2,periodSeconds=5,failureThreshold=24'
上列命令是給讀者參考的示範設定(示範並行 4、逾時 60 秒)[2];本次採用的修訂版截圖顯示為本次實際設定值(並行 80、逾時 300 秒)。實驗的對應值以保存之修訂版設定為準。專案 gen-lang-client-0769172331 為 AI Studio 建立之測試專案。
命令中特別以 /healthz 作為啟動探針(startup-probe),設定檢查預算為 5 × 24 = 120 秒。在容器冷啟動並探針成功之前,平台不會將任何真實流量導向該實例;超出預算仍未就緒則關閉容器。這不代表 LINE 會等 120 秒,也不能代替後端業務的端到端檢查。
LINE Webhook 需要可連入的 HTTPS 端點,所以這裡允許外部呼叫,業務簽章驗證照樣保留。/healthz 回應成功,只代表 HTTP 行程能工作;還要從 LINE 問一次、真的讀寫 Firestore,才知道這條服務有沒有接起來。

圖 3:Cloud Run 修訂版管理畫面。local-day12-agent-00004-zz6 已就緒並配置 100% 流量;這張圖呈現修訂版配置,沒有呈現自然縮容至零的監控紀錄。
驗收一個雲端 Agent 服務時,我們依序檢視六個關鍵狀態:搜尋(意圖解析) → 請求(原文留存) → 同意(驗簽與時限) → 建單(原子寫入) → 替換(流量交棒) → 查回(原單讀回)。
這次的驗證重點是 同一映像的新修訂版任務接續 。先在修訂版 A 建立請求,保存手機畫面與原文件;接著部署相同映像的新修訂版 B,待平台就緒後透過流量分配將 100% 流量交棒給修訂版 B。專案、命名空間、身分金鑰與資料庫保持不變。
接著在手機送出一則「剛才那單有成功嗎?」。應用程式的查回入口讀取持久化 Session 參照,呼叫原 Day 11 的 lookup(),這一步不重新要求確認,也不向模型索取單號。
實測記錄對照表:
| 核對項目 | 修訂版 A(建立請求) | 修訂版 B(查回請求) |
|---|---|---|
K_REVISION |
local-day12-agent-00003-2kh |
local-day12-agent-00004-zz6(承接 100% 流量) |
request_id |
req-20260926-85daad15417821ee |
req-20260926-85daad15417821ee(完全一致) |
| 同一操作對應的請求文件數 | 1 筆 | 1 筆(查回未重複新增) |
兩次訊息處理的行程識別(boot_id)與操作識別(operation_id)已由程式記錄於 Cloud Run 日誌(PROCESS_STARTED 與 BUSINESS_RESULT 事件);兩版修訂版各自獨立啟動,修訂版不同,記憶體也不共用。容器的 PID 很可能同樣是 1,所以不能只拿兩個 PID 當作雲端重啟證明。
00003-2kh 切換到 00004-zz6,兩次請求由不同行程處理;新行程能從 Firestore 讀回同一單號,證明狀態已獨立於單一行程之外。test_core.py 的具名單元測試把關;本次雲端實測則專注走通正常路徑,讓邊界防護與線上主線各自有對應的檢驗依據。Day 9 讓同一把鍵對應同一筆請求,Day 10 處理拿不到回條的情況,Day 11 再保存跨行程接續的依據。Day 12 把這些能力帶到 Cloud Run 與 LINE,讓使用者實際看到「你剛才交代的那件事還在」。
這次先集中在一條教學服務。店家資料按 Day 14 引入;真人接收與完整交付自動化仍各有篇章。服務的正常查詢、確認與查回能在手機上串起來,前面那些工程選擇才真的變成使用者少走的一段路。
接下來再整理 LINE 裡的狀態與操作:使用者看見原單之後,要怎麼一眼知道現在做到哪裡、下一步能按什麼?
前篇:Day 11|服務重啟了,剛才交代的事還在嗎?。本篇完整程式碼與架構說明在 examples/day12/。